package org.skycloud.mybatis;

import java.util.Properties;

import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.MappedStatement.Builder;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.skycloud.mybatis.dialect.Dialect;
import org.skycloud.mybatis.utils.StringBuilderUtils;

@Intercepts({
		@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
				RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class }),
		@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
				RowBounds.class, ResultHandler.class }) })
final class RowBoundsInterceptor implements Interceptor {

	private final Dialect dialect;

	RowBoundsInterceptor(Dialect dialect) {
		this.dialect = dialect;
	}

	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		if (dialect == null) {
			throw new IllegalStateException("dialect is null, please set dialect first.");
		}

		Object[] args = invocation.getArgs();

		RowBounds rowBounds = (RowBounds) args[2];
		if (rowBounds == null
				|| (rowBounds.getOffset() == RowBounds.NO_ROW_OFFSET && rowBounds.getLimit() == RowBounds.NO_ROW_LIMIT)) {
			return invocation.proceed();
		}

		MappedStatement mappedStatement = (MappedStatement) args[0];

		SqlSource sqlSource = new SqlSourceProxy(mappedStatement.getSqlSource(), mappedStatement.getConfiguration(),
				rowBounds, dialect);
		Builder builder = new Builder(mappedStatement.getConfiguration(), mappedStatement.getId(), sqlSource,
				mappedStatement.getSqlCommandType());

		builder.resource(mappedStatement.getResource());
		builder.parameterMap(mappedStatement.getParameterMap());
		builder.resultMaps(mappedStatement.getResultMaps());
		builder.fetchSize(mappedStatement.getFetchSize());
		builder.timeout(mappedStatement.getTimeout());
		builder.statementType(mappedStatement.getStatementType());
		builder.resultSetType(mappedStatement.getResultSetType());
		builder.cache(mappedStatement.getCache());
		builder.flushCacheRequired(mappedStatement.isFlushCacheRequired());
		builder.useCache(mappedStatement.isUseCache());
		builder.resultOrdered(mappedStatement.isResultOrdered());
		builder.keyGenerator(mappedStatement.getKeyGenerator());
		builder.databaseId(mappedStatement.getDatabaseId());
		builder.lang(mappedStatement.getLang());

		String[] keyProperties = mappedStatement.getKeyProperties();
		if (keyProperties != null) {
			StringBuilder buf = StringBuilderUtils.getThreadLocalStringBuilder(0);
			for (int i = 0; i < keyProperties.length; i++) {
				if (i > 0) {
					buf.append(',');
				}
				buf.append(keyProperties[i]);
			}
			builder.keyProperty(buf.toString());
		}

		String[] keyColumns = mappedStatement.getKeyColumns();
		if (keyColumns != null) {
			StringBuilder buf = StringBuilderUtils.getThreadLocalStringBuilder(0);
			for (int i = 0; i < keyColumns.length; i++) {
				if (i > 0) {
					buf.append(',');
				}
				buf.append(keyColumns[i]);
			}
			builder.keyColumn(buf.toString());
		}

		args[0] = builder.build();
		args[2] = RowBounds.DEFAULT;

		return invocation.proceed();
	}

	@Override
	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}

	@Override
	public void setProperties(Properties properties) {
	}

	private static final class SqlSourceProxy implements SqlSource {

		private final SqlSource sqlSource;
		private final Configuration configuration;
		private final RowBounds rowBounds;
		private final Dialect dialect;

		SqlSourceProxy(SqlSource sqlSource, Configuration configuration, RowBounds rowBounds, Dialect dialect) {
			this.sqlSource = sqlSource;
			this.configuration = configuration;
			this.rowBounds = rowBounds;
			this.dialect = dialect;
		}

		@Override
		public BoundSql getBoundSql(Object parameterObject) {
			BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
			return new BoundSql(configuration, createSql(boundSql.getSql()), boundSql.getParameterMappings(),
					boundSql.getParameterObject());
		}

		private String createSql(String sql) {
			return dialect.getSql(sql, rowBounds);
		}
	}
}
